/*******************************************************************************
 * Copyright (c) 2000, 2011 IBM Corporation and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *  IBM Corporation - initial API and implementation
 *  Willian Mitsuda <wmitsuda@gmail.com>
 *     - Fix for bug 196553 - [Dialogs] Support IColorProvider/IFontProvider in FilteredItemsSelectionDialog
 *  Peter Friese <peter.friese@gentleware.com>
 *     - Fix for bug 208602 - [Dialogs] Open Type dialog needs accessible labels
 *  Simon Muschel <smuschel@gmx.de> - bug 258493
 *******************************************************************************/
package org.eclipse.ceylon.ide.eclipse.code.open;

import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.IHandler;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.ProgressMonitorWrapper;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.ActionContributionItem;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.LegacyActionTools;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.viewers.ContentViewer;
import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider.IStyledLabelProvider;
import org.eclipse.jface.viewers.DoubleClickEvent;
import org.eclipse.jface.viewers.IColorProvider;
import org.eclipse.jface.viewers.IContentProvider;
import org.eclipse.jface.viewers.IDoubleClickListener;
import org.eclipse.jface.viewers.IFontProvider;
import org.eclipse.jface.viewers.ILabelDecorator;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.ILabelProviderListener;
import org.eclipse.jface.viewers.ILazyContentProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.LabelProviderChangedEvent;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.StyledCellLabelProvider;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerCell;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.swt.SWT;
import org.eclipse.swt.accessibility.ACC;
import org.eclipse.swt.accessibility.AccessibleAdapter;
import org.eclipse.swt.accessibility.AccessibleEvent;
import org.eclipse.swt.browser.LocationEvent;
import org.eclipse.swt.browser.LocationListener;
import org.eclipse.swt.custom.CLabel;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.custom.StyleRange;
import org.eclipse.swt.custom.ViewForm;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.TraverseEvent;
import org.eclipse.swt.events.TraverseListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Menu;
import org.eclipse.swt.widgets.ProgressBar;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.Text;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;
import org.eclipse.ui.ActiveShellExpression;
import org.eclipse.ui.IMemento;
import org.eclipse.ui.IWorkbenchCommandConstants;
import org.eclipse.ui.IWorkbenchPreferenceConstants;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.WorkbenchException;
import org.eclipse.ui.XMLMemento;
import org.eclipse.ui.dialogs.SearchPattern;
import org.eclipse.ui.dialogs.SelectionStatusDialog;
import org.eclipse.ui.handlers.IHandlerActivation;
import org.eclipse.ui.handlers.IHandlerService;
import org.eclipse.ui.internal.IWorkbenchGraphicConstants;
import org.eclipse.ui.internal.WorkbenchImages;
import org.eclipse.ui.internal.WorkbenchMessages;
import org.eclipse.ui.internal.WorkbenchPlugin;
import org.eclipse.ui.progress.UIJob;
import org.eclipse.ui.statushandlers.StatusManager;

import org.eclipse.ceylon.ide.eclipse.ui.CeylonPlugin;
import org.eclipse.ceylon.ide.eclipse.ui.CeylonResources;
import org.eclipse.ceylon.ide.eclipse.util.DocBrowser;

/**
 * Shows a list of items to the user with a text entry field for a string
 * pattern used to filter the list of items.
 * 
 * @since 3.3
 */
public abstract class FilteredItemsSelectionDialog extends
        SelectionStatusDialog {

    private static final Image MENU_IMAGE = 
            WorkbenchImages.getImage(
                    IWorkbenchGraphicConstants.IMG_LCL_VIEW_MENU);
    private static final Image SEP_IMAGE = 
            WorkbenchImages.getImage(
                    IWorkbenchGraphicConstants.IMG_OBJ_SEPARATOR);

    protected static final String DIALOG_BOUNDS_SETTINGS = "DialogBoundsSettings";

    private static final String SHOW_STATUS_LINE = "ShowStatusLine";
    private static final String SHOW_DOC_AREA = "ShowDocArea";

    private static final String HISTORY_SETTINGS = "History";

    protected static final String DIALOG_HEIGHT = "DIALOG_HEIGHT";

    protected static final String DIALOG_WIDTH = "DIALOG_WIDTH";

    /**
     * Represents an empty selection in the pattern input field (used only for
     * initial pattern).
     */
    public static final int NONE = 0;

    /**
     * Pattern input field selection where caret is at the beginning (used only
     * for initial pattern).
     */
    public static final int CARET_BEGINNING = 1;

    /**
     * Represents a full selection in the pattern input field (used only for
     * initial pattern).
     */
    public static final int FULL_SELECTION = 2;

    private Text pattern;

    private TableViewer list;

    private DetailsContentViewer details;
    private DetailsContentViewer moreDetails;
    private DetailsContentViewer evenMoreDetails;

    /**
     * It is a duplicate of a field in the CLabel class in DetailsContentViewer.
     * It is maintained, because the <code>setDetailsLabelProvider()</code>
     * could be called before content area is created.
     */
    private ILabelProvider detailsLabelProvider;
    private ILabelProvider moreDetailsLabelProvider;
    private ILabelProvider evenMoreDetailsLabelProvider;

    private ItemsListLabelProvider itemsListLabelProvider;

    private MenuManager menuManager;

    private MenuManager contextMenuManager;

    private boolean multi;

    private ToolBar toolBar;

    private ToolItem menuToolItem;
    private ToolItem toggleDocToolItem;

    private ProgressBar progressBar;

    private ToggleStatusLineAction toggleStatusLineAction;
    private ToggleDocAreaAction toggleDocAction;

    private RemoveHistoryItemAction removeHistoryItemAction;

    private ActionContributionItem removeHistoryActionContributionItem;

    private IStatus status;

    private RefreshCacheJob refreshCacheJob;

    private RefreshProgressMessageJob refreshProgressMessageJob = new RefreshProgressMessageJob();

    private Object[] currentSelection;

    private ContentProvider contentProvider;

    private FilterHistoryJob filterHistoryJob;

    private FilterJob filterJob;

    private ItemsFilter filter;

    private List<Object> lastCompletedResult;

    private ItemsFilter lastCompletedFilter;

    private String initialPatternText;

    private int selectionMode;

    private ItemsListSeparator itemsListSeparator;

    private static final String EMPTY_STRING = "";

    private boolean refreshWithLastSelection = false;

    private IHandlerActivation showViewHandler;

    private ViewForm statusArea;
    
    private final String filterLabelText;
    private final String listLabelText;

    private DocBrowser browser;

    private SashForm sash;

    /**
     * Creates a new instance of the class.
     * 
     * @param shell
     *            shell to parent the dialog on
     * @param multi
     *            indicates whether dialog allows to select more than one
     *            position in its list of items
     */
    public FilteredItemsSelectionDialog(Shell shell, boolean multi,
            String filterLabelText, String listLabelText) {
        super(shell);
        this.multi = multi;
        this.filterLabelText = filterLabelText;
        this.listLabelText = listLabelText;
        filterHistoryJob = new FilterHistoryJob();
        filterJob = new FilterJob();
        filterJob.setRule(ResourcesPlugin.getWorkspace().getRoot()); //don't run while we're compiling
        contentProvider = new ContentProvider();
        refreshCacheJob = new RefreshCacheJob();
        itemsListSeparator = new ItemsListSeparator("Workspace matches");
        selectionMode = NONE;
    }
    
    protected void initLabelProviders(ILabelProvider listLabelProvider,
            ILabelDecorator listSelectionLabelDecorator,
            ILabelProvider detailsLabelProvider, 
            ILabelProvider moreDetailsLabelProvider,
            ILabelProvider evenMoreDetailsLabelProvider) {
        itemsListLabelProvider = 
                new ItemsListLabelProvider(listLabelProvider, 
                        listSelectionLabelDecorator);
        this.detailsLabelProvider = detailsLabelProvider;
        this.moreDetailsLabelProvider = moreDetailsLabelProvider;
        this.evenMoreDetailsLabelProvider = evenMoreDetailsLabelProvider;
    }
    
    /**
     * Adds viewer filter to the dialog items list.
     * 
     * @param filter
     *            the new filter
     */
    protected void addListFilter(ViewerFilter filter) {
        contentProvider.addFilter(filter);
    }
    
    @Override
    public void create() {
        super.create();
        pattern.setFocus();
    }
    
    public boolean enableDocArea() {
        return true;
    }

    /**
     * Restores dialog using persisted settings. The default implementation
     * restores the status of the details line and the selection history.
     * 
     * @param settings
     *            settings used to restore dialog
     */
    protected void restoreDialog(IDialogSettings settings) {
        boolean toggleStatusLine = true;

        if (settings.get(SHOW_STATUS_LINE) != null) {
            toggleStatusLine = settings.getBoolean(SHOW_STATUS_LINE);
        }
        
        if (toggleStatusLineAction!=null) {
            toggleStatusLineAction.setChecked(toggleStatusLine);
        }

        setStatusAreaVisible(toggleStatusLine);

        if (enableDocArea()) {
            boolean toggleDocArea = true;

            if (settings.get(SHOW_DOC_AREA) != null) {
                toggleDocArea = settings.getBoolean(SHOW_DOC_AREA);
            }

            if (toggleDocAction!=null) {
                toggleDocAction.setChecked(toggleDocArea);
            }
            if (toggleDocToolItem!=null) {
                toggleDocToolItem.setSelection(toggleDocArea);
            }

            setDocAreaVisible(toggleDocArea);
        }

        String setting = settings.get(HISTORY_SETTINGS);
        if (setting != null) {
            try {
                IMemento memento = 
                        XMLMemento.createReadRoot(
                                new StringReader(setting));
                this.contentProvider.loadHistory(memento);
            }
            catch (WorkbenchException e) {
                // Simply don't restore the settings
                StatusManager
                        .getManager()
                        .handle(new Status(IStatus.ERROR,
                                PlatformUI.PLUGIN_ID,
                                IStatus.ERROR,
                                WorkbenchMessages.FilteredItemsSelectionDialog_restoreError,
                                e));
            }
        }
    }

    @Override
    public boolean close() {
        this.filterJob.cancel();
        this.refreshCacheJob.cancel();
        this.refreshProgressMessageJob.cancel();
        if (showViewHandler != null) {
            IHandlerService service = 
                    (IHandlerService) 
                        PlatformUI.getWorkbench()
                            .getService(IHandlerService.class);
            service.deactivateHandler(showViewHandler);
            showViewHandler.getHandler().dispose();
            showViewHandler = null;
        }
        if (menuManager != null)
            menuManager.dispose();
        if (contextMenuManager != null)
            contextMenuManager.dispose();
        storeDialog(getDialogSettings());
        if (enableDocArea()) {
            browser.close();
        }
        return super.close();
    }

    /**
     * Stores dialog settings.
     * 
     * @param settings
     *            settings used to store dialog
     */
    protected void storeDialog(IDialogSettings settings) {
        settings.put(SHOW_STATUS_LINE, 
                statusArea.isVisible());
        
        if (enableDocArea()) {
            settings.put(SHOW_DOC_AREA, 
                    browser!=null && browser.isVisible());
        }

        XMLMemento memento = 
                XMLMemento.createWriteRoot(HISTORY_SETTINGS);
        this.contentProvider.saveHistory(memento);
        StringWriter writer = new StringWriter();
        try {
            memento.save(writer);
            settings.put(HISTORY_SETTINGS, 
                    writer.getBuffer().toString());
        } catch (IOException e) {
            // Simply don't store the settings
            StatusManager
                    .getManager()
                    .handle(
                            new Status(
                                    IStatus.ERROR,
                                    PlatformUI.PLUGIN_ID,
                                    IStatus.ERROR,
                                    WorkbenchMessages.FilteredItemsSelectionDialog_storeError,
                                    e));
        }
    }

    /**
     * Create a new header which is labelled by headerLabel.
     * 
     * @param parent
     * @return Label the label of the header
     */
    private Label createHeader(Composite parent) {
        Composite header = new Composite(parent, SWT.NONE);
        header.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

        GridLayout layout = new GridLayout();
        layout.numColumns = 3;
        layout.marginWidth = 0;
        layout.marginHeight = 0;
        header.setLayout(layout);

        Label headerLabel = new Label(header, SWT.NONE);
        headerLabel.setText(filterLabelText);
        headerLabel.addTraverseListener(new TraverseListener() {
            @Override
            public void keyTraversed(TraverseEvent e) {
                if (e.detail == SWT.TRAVERSE_MNEMONIC && e.doit) {
                    e.detail = SWT.TRAVERSE_NONE;
                    pattern.setFocus();
                }
            }
        });
        headerLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));

        /*ToolBar toolBar = new ToolBar(header, SWT.FLAT);
        GridData data = new GridData();
        data.horizontalAlignment = GridData.END;
        toolBar.setLayoutData(data);
        
        createToolBar(toolBar);*/
        
        createViewMenu(header);
        
        return headerLabel;
    }

    /*protected void createToolBar(ToolBar toolBar) {        
        toggleDocToolItem = new ToolItem(toolBar, SWT.CHECK, 0);
        toggleDocToolItem.setImage(DOC_IMAGE);
        toggleDocToolItem.setToolTipText("Show/Hide Documentation");
        toggleDocToolItem.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                boolean selection = toggleDocToolItem.getSelection();
                setDocAreaVisible(selection);
                if (toggleDocAction!=null) {
                    toggleDocAction.setChecked(selection);
                }
            }
        });
    }*/

    /**
     * Create the labels for the list and the progress. 
     * Return the list label.
     * 
     * @param parent
     * @return Label
     */
    private Label createLabels(Composite parent) {
        Composite labels = new Composite(parent, SWT.NONE);
        labels.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        labels.setLayout(GridLayoutFactory.fillDefaults()
                .equalWidth(false)
                .numColumns(2)
                .margins(0, 0)
                .create());
        
        Label listLabel = new Label(labels, SWT.NONE);
        listLabel.setText(listLabelText);
        listLabel.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
        listLabel.addTraverseListener(new TraverseListener() {
            @Override
            public void keyTraversed(TraverseEvent e) {
                if (e.detail == SWT.TRAVERSE_MNEMONIC && e.doit) {
                    e.detail = SWT.TRAVERSE_NONE;
                    list.getTable().setFocus();
                }
            }
        });        

        progressBar = new ProgressBar(labels, SWT.RIGHT | SWT.SMOOTH);
        GridData gridData = new GridData(GridData.FILL_HORIZONTAL);
        gridData.exclude = true;
        progressBar.setLayoutData(gridData);
        progressBar.setVisible(false);
        
        return listLabel;
    }
    
    private void createViewMenu(Composite parent) {
        toolBar = new ToolBar(parent, SWT.FLAT);
        
        menuToolItem = new ToolItem(toolBar, SWT.PUSH, 0);
        menuToolItem.setImage(MENU_IMAGE);
        menuToolItem.setToolTipText("Menu");
        menuToolItem.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                showViewMenu();
            }
        });
        
        GridData data = new GridData();
        data.horizontalAlignment = GridData.END;
        toolBar.setLayoutData(data);

        toolBar.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseDown(MouseEvent e) {
                showViewMenu();
            }
        });

        menuManager = new MenuManager();

        fillViewMenu(menuManager);
        toolBar.setVisible(!menuManager.isEmpty());
        
        IHandlerService service = 
                (IHandlerService) 
                    PlatformUI.getWorkbench()
                        .getService(IHandlerService.class);
        IHandler handler = new AbstractHandler() {
            @Override
            public Object execute(ExecutionEvent event) {
                showViewMenu();
                return null;
            }
        };
        showViewHandler = service.activateHandler(
                IWorkbenchCommandConstants.WINDOW_SHOW_VIEW_MENU, handler,
                new ActiveShellExpression(getShell()));
    }

    /**
     * Fills the menu of the dialog.
     * 
     * @param menuManager
     *            the menu manager
     */
    protected void fillViewMenu(IMenuManager menuManager) {
        toggleStatusLineAction = new ToggleStatusLineAction();
        menuManager.add(toggleStatusLineAction);
        
        if (enableDocArea()) {
            toggleDocAction = new ToggleDocAreaAction();
            menuManager.add(toggleDocAction);
        }
    }

    private void showViewMenu() {
        Menu menu = menuManager.createContextMenu(getShell());
        Rectangle bounds = menuToolItem.getBounds();
        Point topLeft = 
                new Point(bounds.x, bounds.y + bounds.height);
        topLeft = toolBar.toDisplay(topLeft);
        menu.setLocation(topLeft.x, topLeft.y);
        menu.setVisible(true);
    }

    /**
     * Hook that allows to add actions to the context menu.
     * <p>
     * Subclasses may extend in order to add other actions.</p>
     * 
     * @param menuManager the context menu manager
     * @since 3.5
     */
    protected void fillContextMenu(IMenuManager menuManager) {
        List selectedElements = 
                ((StructuredSelection)list.getSelection()).toList();

        Object item= null;

        for (Iterator it= selectedElements.iterator(); it.hasNext();) {
            item= it.next();
            if (item instanceof ItemsListSeparator || !isHistoryElement(item)) {
                return;
            }
        }

        if (selectedElements.size() > 0) {
            removeHistoryItemAction.setText(WorkbenchMessages.FilteredItemsSelectionDialog_removeItemsFromHistoryAction);

            menuManager.add(removeHistoryActionContributionItem);

        }
    }

    private void createPopupMenu() {
        removeHistoryItemAction = new RemoveHistoryItemAction();
        removeHistoryActionContributionItem = 
                new ActionContributionItem(removeHistoryItemAction);

        contextMenuManager = new MenuManager();
        contextMenuManager.setRemoveAllWhenShown(true);
        contextMenuManager.addMenuListener(new IMenuListener() {
            @Override
            public void menuAboutToShow(IMenuManager manager) {
                fillContextMenu(manager);
            }
        });

        final Table table = list.getTable();
        Menu menu= contextMenuManager.createContextMenu(table);
        table.setMenu(menu);
    }

    @Override
    protected Control createDialogArea(final Composite parent) {
        final Composite dialogArea = 
                (Composite) super.createDialogArea(parent);

        final Composite content = 
                new Composite(dialogArea, SWT.NONE);
        content.setLayoutData(new GridData(GridData.FILL_BOTH));

        GridLayout layout = new GridLayout();
        layout.numColumns = 1;
        layout.marginWidth = 0;
        layout.marginHeight = 0;
        content.setLayout(layout);

        createPatternInput(content);
        
        final Label listLabel = createLabels(content);

        sash = new SashForm(content, SWT.HORIZONTAL | SWT.SMOOTH);
        sash.setLayoutData(new GridData(GridData.FILL_BOTH));
        Composite composite = new Composite(sash, SWT.NONE);
        composite.setLayoutData(new GridData(GridData.FILL_BOTH));
        composite.setLayout(GridLayoutFactory.fillDefaults().create());
        list = new TableViewer(composite, 
                (multi ? SWT.MULTI : SWT.SINGLE)
                | SWT.BORDER | SWT.V_SCROLL | SWT.VIRTUAL);
        list.getTable()
            .getAccessible()
            .addAccessibleListener(
                new AccessibleAdapter() {
                    @Override
                    public void getName(AccessibleEvent e) {
                        if (e.childID == ACC.CHILDID_SELF) {
                            e.result = LegacyActionTools
                                    .removeMnemonics(listLabel.getText());
                        }
                    }
                });
        list.setContentProvider(contentProvider);
        list.setLabelProvider(itemsListLabelProvider);
        list.setInput(new Object[0]);
        list.setItemCount(contentProvider.getNumberOfElements());
        applyDialogFont(list.getTable());
        GridData gd = new GridData(GridData.FILL_BOTH);
        gd.heightHint = list.getTable().getItemHeight() * 15;
        list.getTable().setLayoutData(gd);
        
        if (enableDocArea()) {
            browser = new DocBrowser(sash, SWT.BORDER);
            browser.addLocationListener(new LocationListener() {
                @Override
                public void changing(LocationEvent event) {
                    String location = event.location;
                    //necessary for windows environment (fix for blank page)
                    //somehow related to this: https://bugs.eclipse.org/bugs/show_bug.cgi?id=129236
                    if (!"about:blank".equals(location) && 
                            !location.startsWith("http:")) {
                        event.doit= false;
                        handleLink(event.location, browser);
                    }
                }
                @Override
                public void changed(LocationEvent event) {}
            });
            refreshBrowserContent(browser, null);
            setDocAreaVisible(toggleDocAction==null ||
                    toggleDocAction.isChecked());            
        }
        
        createPopupMenu();
        
        pattern.addModifyListener(new ModifyListener() {
            @Override
            public void modifyText(ModifyEvent e) {
                applyFilter();
            }
        });

        pattern.addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent e) {
                if (e.keyCode == SWT.ARROW_DOWN) {
                    if (list.getTable().getItemCount() > 0) {
                        list.getTable().setFocus();
                    }
                }
            }
        });

        list.addSelectionChangedListener(
                new ISelectionChangedListener() {
            boolean first = true;
            @Override
            public void selectionChanged(SelectionChangedEvent event) {
                StructuredSelection selection = 
                        (StructuredSelection) 
                            event.getSelection();
                handleSelected(selection);
                //we need to re-layout the status area *only* the 
                //very first time an icon is displayed!
                if (first && selection!=null && !selection.isEmpty()) {
                    statusArea.getParent().layout();
                    first = false;
                }
            }
        });

        list.addDoubleClickListener(new IDoubleClickListener() {
            @Override
            public void doubleClick(DoubleClickEvent event) {
                handleDoubleClick();
            }
        });

        list.getTable().addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed(KeyEvent e) {
                
                char character = e.character;
                int keyCode = e.keyCode;
                int stateMask = e.stateMask;
                
                if (stateMask==SWT.NONE || stateMask==SWT.SHIFT) {
                    if (Character.isLetter(character) 
                            && keyCode==Character.toLowerCase(character)) {
                        String string = pattern.getText() + character;
                        pattern.setText(string);
                        pattern.setFocus();
                        pattern.setSelection(string.length());
                    }
                    if (character == SWT.BS && keyCode==SWT.BS) {
                        String string = pattern.getText();
                        if (string.length()>0) {
                            string = string.substring(0,string.length()-1);
                        }
                        pattern.setText(string);
                        pattern.setFocus();
                        pattern.setSelection(string.length());
                    }
                }
                
                if (keyCode == SWT.DEL) {

                    List selectedElements = ((StructuredSelection) list
                            .getSelection()).toList();

                    Object item = null;
                    boolean isSelectedHistory = true;

                    for (Iterator it = selectedElements.iterator(); it
                            .hasNext();) {
                        item = it.next();
                        if (item instanceof ItemsListSeparator
                                || !isHistoryElement(item)) {
                            isSelectedHistory = false;
                            break;
                        }
                    }
                    if (isSelectedHistory)
                        removeSelectedItems(selectedElements);

                }

                if (keyCode == SWT.ARROW_UP && (stateMask & SWT.SHIFT) != 0
                        && (stateMask & SWT.CTRL) != 0) {
                    StructuredSelection selection = 
                            (StructuredSelection) 
                                list.getSelection();

                    if (selection.size() == 1) {
                        Object element = selection.getFirstElement();
                        if (element.equals(list.getElementAt(0))) {
                            pattern.setFocus();
                        }
                        if (list.getElementAt(list.getTable()
                                .getSelectionIndex() - 1) instanceof ItemsListSeparator)
                            list.getTable().setSelection(
                                    list.getTable().getSelectionIndex() - 1);
                        list.getTable().notifyListeners(SWT.Selection,
                                new Event());

                    }
                }

                if (keyCode == SWT.ARROW_DOWN
                        && (stateMask & SWT.SHIFT) != 0
                        && (stateMask & SWT.CTRL) != 0) {

                    if (list.getElementAt(list.getTable().getSelectionIndex() + 1) 
                            instanceof ItemsListSeparator)
                        list.getTable().setSelection(
                                list.getTable().getSelectionIndex() + 1);
                    list.getTable().notifyListeners(SWT.Selection, new Event());
                }

            }
        });
        
        createStatusArea(content);
        
        applyDialogFont(content);

        restoreDialog(getDialogSettings());

        if (initialPatternText != null) {
            pattern.setText(initialPatternText);
        }

        switch (selectionMode) {
        case CARET_BEGINNING:
            pattern.setSelection(0, 0);
            break;
        case FULL_SELECTION:
            pattern.setSelection(0, initialPatternText.length());
            break;
        }

        // apply filter even if pattern is empty (display history)
        applyFilter();
        
        return dialogArea;
    }

    protected void createPatternInput(final Composite content) {
        GridData gd;
        final Label headerLabel = createHeader(content);

        pattern = new Text(content, SWT.SINGLE | SWT.BORDER | SWT.SEARCH | SWT.ICON_SEARCH | SWT.ICON_CANCEL);
        pattern.getAccessible().addAccessibleListener(new AccessibleAdapter() {
            @Override
            public void getName(AccessibleEvent e) {
                e.result = LegacyActionTools.removeMnemonics(headerLabel.getText());
            }
        });
        gd = new GridData(GridData.FILL_HORIZONTAL);
        pattern.setLayoutData(gd);
    }

    protected void createStatusArea(final Composite parent) {
        statusArea = new ViewForm(parent, /*SWT.BORDER |*/ SWT.FLAT);
        statusArea.setEnabled(false);
        statusArea.setLayoutData(GridDataFactory.fillDefaults().span(2, 1).create());
        final Composite content = new Composite(statusArea, SWT.NONE) {
            @Override
            public Point computeSize(int wh, int hh, boolean changed) {
                //ensures that menu dropdown stays on the window
                return new Point(10, super.computeSize(wh, hh, changed).y);
            }
            @Override
            protected void checkSubclass() {
                // noop, allow subclassing, since we know what we are doing
            }
        };
        content.setLayout(GridLayoutFactory.fillDefaults().numColumns(3).equalWidth(false).spacing(0, 0).create());
        content.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).create());
        statusArea.setContent(content);
        
        statusArea.setBackground(parent.getBackground());
        statusArea.setForeground(parent.getForeground());
        content.setBackground(parent.getBackground());
        content.setForeground(parent.getForeground());
        
        setStatusAreaVisible(toggleStatusLineAction==null ||
                toggleStatusLineAction.isChecked());
        
        if (detailsLabelProvider!=null) {
            details = new DetailsContentViewer(content, /*SWT.BORDER |*/ SWT.FLAT);
            details.setContentProvider(new NullContentProvider());
            details.setLabelProvider(detailsLabelProvider);
        }

        if (moreDetailsLabelProvider!=null) {
            moreDetails = new DetailsContentViewer(content, /*SWT.BORDER |*/ SWT.FLAT);
            moreDetails.setContentProvider(new NullContentProvider());
            moreDetails.setLabelProvider(moreDetailsLabelProvider);
        }
        
        if (evenMoreDetailsLabelProvider!=null) {
            evenMoreDetails = new DetailsContentViewer(content, /*SWT.BORDER |*/ SWT.FLAT);
            evenMoreDetails.setContentProvider(new NullContentProvider());
            evenMoreDetails.setLabelProvider(evenMoreDetailsLabelProvider);
        }
        
        statusArea.addControlListener(new ControlAdapter() {
            @Override
            public void controlResized(ControlEvent e) {
                parent.layout();
                content.layout();
            }
        });
    }

    /**
     * Shows/hides the content viewer.
     * 
     * @param visible
     *            if the content viewer should be visible.
     */
    public void setStatusAreaVisible(boolean visible) {
        statusArea.setVisible(visible);
        GridData gd = (GridData) statusArea.getLayoutData();
        gd.exclude = !visible;
        statusArea.getParent().layout();
        ((Composite) statusArea.getContent()).layout();
    }

    public void setDocAreaVisible(boolean visible) {
        browser.setVisible(visible);
        sash.setWeights(visible ? new int[] {3,2} : new int[] {1,0});
        if (visible) {
            refreshBrowserContent(browser, currentSelection);
        }
    }

    /**
     * This method is a hook for subclasses to override default dialog behavior.
     * The <code>handleDoubleClick()</code> method handles double clicks on
     * the list of filtered elements.
     * <p>
     * Current implementation makes double-clicking on the list do the same as
     * pressing <code>OK</code> button on the dialog.
     */
    protected void handleDoubleClick() {
        okPressed();
    }

    /**
     * Refreshes the details field according to the current selection in the
     * items list.
     */
    private void refreshDetails() {
        StructuredSelection selection = getSelectedItems();
        switch (selection.size()) {
        case 0:
            if (details!=null) details.setInput(null);
            if (moreDetails!=null) moreDetails.setInput(null);
            if (evenMoreDetails!=null) evenMoreDetails.setInput(null);
            break;
        case 1:
            Object firstElement = selection.getFirstElement();
            if (details!=null) details.setInput(firstElement);
            if (moreDetails!=null) moreDetails.setInput(firstElement);
            if (evenMoreDetails!=null) evenMoreDetails.setInput(firstElement);
            break;
        default:
            if (details!=null) details.setInput(selection.size() + " items selected");
            if (moreDetails!=null) moreDetails.setInput(null);
            if (evenMoreDetails!=null) evenMoreDetails.setInput(null);
            break;
        }
        ((Composite) statusArea.getContent()).layout();
    }

    
    /**
     * Handle selection in the items list by updating labels of selected and
     * unselected items and refresh the details field using the selection.
     * 
     * @param selection
     *            the new selection
     */
    protected void handleSelected(StructuredSelection selection) {
        IStatus status = 
                new Status(IStatus.OK, 
                        PlatformUI.PLUGIN_ID,
                        IStatus.OK, 
                        EMPTY_STRING, null);

        Object[] lastSelection = currentSelection;

        currentSelection = selection.toArray();

        if (selection.size() == 0) {
            status = 
                    new Status(IStatus.ERROR, 
                            PlatformUI.PLUGIN_ID,
                            IStatus.ERROR, 
                            EMPTY_STRING, null);

            if (lastSelection != null
                    /*&& getListSelectionLabelDecorator() != null*/) {
                list.update(lastSelection, null);
            }

            currentSelection = null;

        } else {
            status = 
                    new Status(IStatus.ERROR, 
                            PlatformUI.PLUGIN_ID,
                            IStatus.ERROR, 
                            EMPTY_STRING, null);

            List items = selection.toList();

            Object item = null;
            IStatus tempStatus = null;

            for (Iterator it = items.iterator(); 
                    it.hasNext();) {
                Object o = it.next();

                if (o instanceof ItemsListSeparator) {
                    continue;
                }

                item = o;
                tempStatus = validateItem(item);

                if (tempStatus.isOK()) {
                    status = 
                            new Status(IStatus.OK, 
                                    PlatformUI.PLUGIN_ID,
                                    IStatus.OK, 
                                    EMPTY_STRING, null);
                } else {
                    status = tempStatus;
                    // if any selected element is not valid status is set to
                    // ERROR
                    break;
                }
            }

            if (lastSelection != null
                    /*&& getListSelectionLabelDecorator() != null*/) {
                list.update(lastSelection, null);
            }

//            if (getListSelectionLabelDecorator() != null) {
                list.update(currentSelection, null);
//            }
        }
        
        if (enableDocArea()) {
            refreshBrowserContent(browser, currentSelection);
        }
        
        refreshDetails();
        updateStatus(status);
    }

    protected void refreshBrowserContent(DocBrowser browser, 
            Object[] currentSelection) {}

    @Override
    protected IDialogSettings getDialogBoundsSettings() {
        IDialogSettings settings = getDialogSettings();
        IDialogSettings section = 
                settings.getSection(DIALOG_BOUNDS_SETTINGS);
        if (section == null) {
            section = settings.addNewSection(DIALOG_BOUNDS_SETTINGS);
            section.put(DIALOG_HEIGHT, 450);
            section.put(DIALOG_WIDTH, 750);
        }
        return section;
    }

    /**
     * Returns the dialog settings. Returned object can't be null.
     * 
     * @return return dialog settings for this dialog
     */
    protected abstract IDialogSettings getDialogSettings();

    /**
     * Refreshes the dialog - has to be called in UI thread.
     */
    public void refresh() {
        if (list != null) {
            Table table = list.getTable();
            if (!table.isDisposed()) {
                List lastRefreshSelection = 
                        ((StructuredSelection) list
                                .getSelection()).toList();
                table.deselectAll();

                list.setItemCount(contentProvider.getNumberOfElements());
                list.refresh();

                if (table.getItemCount() > 0) {
                    // preserve previous selection
                    if (refreshWithLastSelection && 
                            lastRefreshSelection != null && 
                            lastRefreshSelection.size() > 0) {
                        list.setSelection(
                                new StructuredSelection(
                                        lastRefreshSelection));
                    }
                    else {
                        refreshWithLastSelection = true;
                        table.setSelection(0);
                        table.notifyListeners(SWT.Selection, 
                                new Event());
                    }
                } else {
                    list.setSelection(StructuredSelection.EMPTY);
                }
            }
        }

        scheduleProgressMessageRefresh();
    }

    /**
     * Notifies the content provider - fires filtering of content provider
     * elements. During the filtering, a separator between history and workspace
     * matches is added.
     * <p>
     * This is a long running operation and should be called in a job.
     * 
     * @param checkDuplicates
     *            <code>true</code> if data concerning elements duplication
     *            should be computed - it takes much more time than the standard
     *            filtering
     * @param monitor
     *            a progress monitor or <code>null</code> if no monitor is
     *            available
     */
    public void reloadCache(boolean checkDuplicates, 
            IProgressMonitor monitor) {
        if (list != null && !list.getTable().isDisposed()
                && contentProvider != null) {
            contentProvider.reloadCache(checkDuplicates, monitor);
        }
    }

    /**
     * Schedule refresh job.
     */
    public void scheduleRefresh() {
        refreshCacheJob.cancelAll();
        refreshCacheJob.schedule();
    }

    /**
     * Schedules progress message refresh.
     */
    public void scheduleProgressMessageRefresh() {
        if (filterJob.getState() != Job.RUNNING && 
                refreshProgressMessageJob.getState() != Job.RUNNING)
            refreshProgressMessageJob.scheduleProgressRefresh(null);
    }

    @Override
    protected void computeResult() {

        StructuredSelection selection = 
                (StructuredSelection) 
                    list.getSelection();
        List<Object> objectsToReturn = 
                new ArrayList<Object>();
        Object item = null;
        List selectedItems = selection.toList();
        for (Iterator it = selectedItems.iterator(); 
                it.hasNext();) {
            item = it.next();
            if (!(item instanceof ItemsListSeparator)) {
                accessedHistoryItem(item);
                objectsToReturn.add(item);
            }
        }

        setResult(objectsToReturn);
    }

    @Override
    protected void updateStatus(IStatus status) {
        this.status = status;
        super.updateStatus(status);
    }

    @Override
    protected void okPressed() {
        if (status != null && 
                (status.isOK() || 
                        status.getCode() == IStatus.INFO)) {
            super.okPressed();
        }
    }

    /**
     * Sets the initial pattern used by the filter. This text is copied into the
     * selection input on the dialog. A full selection is used in the pattern
     * input field.
     * 
     * @param text
     *            initial pattern for the filter
     * @see FilteredItemsSelectionDialog#FULL_SELECTION
     */
    public void setInitialPattern(String text) {
        setInitialPattern(text, FULL_SELECTION);
    }

    /**
     * Sets the initial pattern used by the filter. This text is copied into the
     * selection input on the dialog. The <code>selectionMode</code> is used
     * to choose selection type for the input field.
     * 
     * @param text
     *            initial pattern for the filter
     * @param selectionMode
     *            one of: {@link FilteredItemsSelectionDialog#NONE},
     *            {@link FilteredItemsSelectionDialog#CARET_BEGINNING},
     *            {@link FilteredItemsSelectionDialog#FULL_SELECTION}
     */
    public void setInitialPattern(String text, int selectionMode) {
        this.initialPatternText = text;
        this.selectionMode = selectionMode;
    }

    /**
     * Gets initial pattern.
     * 
     * @return initial pattern, or <code>null</code> if initial pattern is not
     *         set
     */
    protected String getInitialPattern() {
        return this.initialPatternText;
    }

    /**
     * Returns the current selection.
     * 
     * @return the current selection
     */
    protected StructuredSelection getSelectedItems() {

        StructuredSelection selection = 
                (StructuredSelection) 
                    list.getSelection();
        List selectedItems = selection.toList();
        Object itemToRemove = null;
        for (Iterator it = selection.iterator(); 
                it.hasNext();) {
            Object item = it.next();
            if (item instanceof ItemsListSeparator) {
                itemToRemove = item;
                break;
            }
        }
        
        if (itemToRemove == null)
            return new StructuredSelection(selectedItems);
        // Create a new selection without the collision
        List<Object> newItems = 
                new ArrayList<Object>(selectedItems);
        newItems.remove(itemToRemove);
        return new StructuredSelection(newItems);

    }

    /**
     * Validates the item. When items on the items list are 
     * selected or deselected, it validates each item in the 
     * selection and the dialog status depends on all 
     * validations.
     * 
     * @param item
     *            an item to be checked
     * @return status of the dialog to be set
     */
    protected abstract IStatus validateItem(Object item);

    /**
     * Creates an instance of a filter.
     * 
     * @return a filter for items on the items list. Can be 
     *         <code>null</code>, no filtering will be 
     *         applied then, causing no item to be shown in
     *         the list.
     */
    protected abstract ItemsFilter createFilter();

    /**
     * Applies the filter created by <code>createFilter()</code> 
     * method to the items list. When new filter is different 
     * than previous one it will cause refiltering.
     */
    protected void applyFilter() {

        ItemsFilter newFilter = createFilter();

        // don't apply filtering for patterns which mean the same, for example:
        // *a**b and ***a*b
        if (filter != null && 
                filter.equalsFilter(newFilter)) {
            return;
        }

        filterHistoryJob.cancel();
        filterJob.cancel();

        this.filter = newFilter;

        if (this.filter != null) {
            filterHistoryJob.schedule();
        }
    }

    /**
     * Returns comparator to sort items inside content provider. Returned object
     * will be probably created as an anonymous class. Parameters passed to the
     * <code>compare(java.lang.Object, java.lang.Object)</code> are going to
     * be the same type as the one used in the content provider.
     * 
     * @return comparator to sort items content provider
     */
    protected abstract Comparator<Object> getItemsComparator();

    /**
     * Fills the content provider with matching items.
     * 
     * @param contentProvider
     *            collector to add items to.
     *            {@link FilteredItemsSelectionDialog.AbstractContentProvider#add(Object, FilteredItemsSelectionDialog.ItemsFilter)}
     *            only adds items that pass the given <code>itemsFilter</code>.
     * @param itemsFilter
     *            the items filter
     * @param progressMonitor
     *            must be used to report search progress. The state of this
     *            progress monitor reflects the state of the filtering process.
     * @throws CoreException
     */
    protected abstract void fillContentProvider(
            AbstractContentProvider contentProvider, 
            ItemsFilter itemsFilter,
            IProgressMonitor progressMonitor) 
                    throws CoreException;

    /**
     * Removes selected items from history.
     * 
     * @param items
     *            items to be removed
     */
    private void removeSelectedItems(List<Object> items) {
        for (Iterator<Object> iter = items.iterator(); 
                iter.hasNext();) {
            Object item = iter.next();
            removeHistoryItem(item);
        }
        refreshWithLastSelection = false;
        contentProvider.refresh();
    }

    /**
     * Removes an item from history.
     * 
     * @param item
     *            an item to remove
     * @return removed item
     */
    protected Object removeHistoryItem(Object item) {
        return contentProvider.removeHistoryElement(item);
    }

    /**
     * Adds item to history.
     * 
     * @param item
     *            the item to be added
     */
    protected void accessedHistoryItem(Object item) {
        contentProvider.addHistoryElement(item);
    }

    /**
     * Returns a history comparator.
     * 
     * @return decorated comparator
     */
    private Comparator<Object> getHistoryComparator() {
        return new HistoryComparator();
    }

    /**
     * Returns the history of selected elements.
     * 
     * @return history of selected elements, or <code>null</code> if it is not
     *         set
     */
    protected SelectionHistory getSelectionHistory() {
        return this.contentProvider.getSelectionHistory();
    }

    /**
     * Sets new history.
     * 
     * @param selectionHistory
     *            the history
     */
    protected void setSelectionHistory(SelectionHistory selectionHistory) {
        if (this.contentProvider != null)
            this.contentProvider.setSelectionHistory(selectionHistory);
    }

    /**
     * Indicates whether the given item is a history item.
     * 
     * @param item
     *            the item to be investigated
     * @return <code>true</code> if the given item exists in history,
     *         <code>false</code> otherwise
     */
    public boolean isHistoryElement(Object item) {
        return this.contentProvider.isHistoryElement(item);
    }

    /**
     * Indicates whether the given item is a duplicate.
     * 
     * @param item
     *            the item to be investigated
     * @return <code>true</code> if the item is duplicate, <code>false</code>
     *         otherwise
     */
    public boolean isDuplicateElement(Object item) {
        return this.contentProvider.isDuplicateElement(item);
    }

    /**
     * Sets separator label
     * 
     * @param separatorLabel
     *            the label showed on separator
     */
    public void setSeparatorLabel(String separatorLabel) {
        this.itemsListSeparator = new ItemsListSeparator(separatorLabel);
    }

    /**
     * Returns name for then given object.
     * 
     * @param item
     *            an object from the content provider. Subclasses should pay
     *            attention to the passed argument. They should either only pass
     *            objects of a known type (one used in content provider) or make
     *            sure that passed parameter is the expected one (by type
     *            checking like <code>instanceof</code> inside the method).
     * @return name of the given item
     */
    public abstract String getElementName(Object item);

    private class ToggleStatusLineAction extends Action {

        /**
         * Creates a new instance of the class.
         */
        public ToggleStatusLineAction() {
            super("&Show Status Line", IAction.AS_CHECK_BOX);
        }

        @Override
        public void run() {
            setStatusAreaVisible(isChecked());
        }
    }

    private class ToggleDocAreaAction extends Action {
        public ToggleDocAreaAction() {
            super("Show &Documentation", IAction.AS_CHECK_BOX);
            ImageDescriptor descriptor = 
                    CeylonPlugin.imageRegistry()
                        .getDescriptor(CeylonResources.SHOW_DOC);
            setImageDescriptor(descriptor);
        }
        @Override
        public void run() {
            boolean checked = isChecked();
            setDocAreaVisible(checked);
            if (toggleDocToolItem!=null) {
                toggleDocToolItem.setSelection(checked);
            }
        }
    }

    /**
     * Only refreshes UI on the basis of an already sorted and filtered set of
     * items.
     * <p>
     * Standard invocation scenario:
     * <ol>
     * <li>filtering job (<code>FilterJob</code> class extending
     * <code>Job</code> class)</li>
     * <li>cache refresh without checking for duplicates (<code>RefreshCacheJob</code>
     * class extending <code>Job</code> class)</li>
     * <li>UI refresh (<code>RefreshJob</code> class extending
     * <code>UIJob</code> class)</li>
     * <li>cache refresh with checking for duplicates (<cod>CacheRefreshJob</code>
     * class extending <code>Job</code> class)</li>
     * <li>UI refresh (<code>RefreshJob</code> class extending <code>UIJob</code>
     * class)</li>
     * </ol>
     * The scenario is rather complicated, but it had to be applied, because:
     * <ul>
     * <li> refreshing cache is rather a long action and cannot be run in the UI -
     * cannot be run in a UIJob</li>
     * <li> refreshing cache checking for duplicates is twice as long as
     * refreshing cache without checking for duplicates; results of the search
     * could be displayed earlier</li>
     * <li> refreshing the UI have to be run in a UIJob</li>
     * </ul>
     * 
     * @see org.eclipse.ui.dialogs.FilteredItemsSelectionDialog.FilterJob
     * @see org.eclipse.ui.dialogs.FilteredItemsSelectionDialog.RefreshJob
     * @see org.eclipse.ui.dialogs.FilteredItemsSelectionDialog.RefreshCacheJob
     */
    private class RefreshJob extends UIJob {

        /**
         * Creates a new instance of the class.
         */
        public RefreshJob() {
            super(FilteredItemsSelectionDialog.this.getParentShell()
                    .getDisplay(),
                    WorkbenchMessages.FilteredItemsSelectionDialog_refreshJob);
            setSystem(true);
        }

        @Override
        public IStatus runInUIThread(IProgressMonitor monitor) {
            if (monitor.isCanceled())
                return new Status(IStatus.OK, 
                        WorkbenchPlugin.PI_WORKBENCH,
                        IStatus.OK, EMPTY_STRING, null);
            
            FilteredItemsSelectionDialog.this.refresh();
            
            return new Status(IStatus.OK, 
                    PlatformUI.PLUGIN_ID, IStatus.OK,
                    EMPTY_STRING, null);
        }

    }

    /**
     * Refreshes the progress message cyclically with 500 milliseconds delay.
     * <code>RefreshProgressMessageJob</code> is strictly connected with
     * <code>GranualProgressMonitor</code> and use it to to get progress
     * message and to decide about break of cyclical refresh.
     */
    private class RefreshProgressMessageJob extends UIJob {

        private GranualProgressMonitor progressMonitor;

        /**
         * Creates a new instance of the class.
         */
        public RefreshProgressMessageJob() {
            super(FilteredItemsSelectionDialog.this.getParentShell().getDisplay(),
                  WorkbenchMessages.FilteredItemsSelectionDialog_progressRefreshJob);
            setSystem(true);
        }

        @Override
        public IStatus runInUIThread(IProgressMonitor monitor) {

            if (!progressBar.isDisposed()) {
                if (progressMonitor==null || 
                        progressMonitor.isDone() || 
                        progressMonitor.isCanceled()) {
                    progressBar.setVisible(false);
                    ((GridData)progressBar.getLayoutData()).exclude = true;
                }
                else {
                    progressBar.setMaximum(progressMonitor.totalWork*100);
                    progressBar.setSelection((int) (progressMonitor.worked*100));
                    progressBar.setVisible(true);
                    ((GridData)progressBar.getLayoutData()).exclude = false;
                }
//                progressLabel.setText(progressMonitor != null ? progressMonitor
//                        .getMessage() : EMPTY_STRING);
                progressBar.getParent().layout();
            }

            if (progressMonitor == null || 
                    progressMonitor.isDone()) {
                return new Status(IStatus.CANCEL, 
                        PlatformUI.PLUGIN_ID, IStatus.CANCEL, 
                        EMPTY_STRING, null);
            }

            // Schedule cyclical with 500 milliseconds delay
            schedule(100);

            return new Status(IStatus.OK, 
                    PlatformUI.PLUGIN_ID, IStatus.OK,
                    EMPTY_STRING, null);
        }

        /**
         * Schedule progress refresh job.
         * 
         * @param progressMonitor
         *            used during refresh progress label
         */
        public void scheduleProgressRefresh(
                GranualProgressMonitor progressMonitor) {
            this.progressMonitor = progressMonitor;
            // Schedule with initial delay to avoid flickering when the user
            // types quickly
            schedule(200);
        }

    }

    /**
     * A job responsible for computing filtered items list presented using
     * <code>RefreshJob</code>.
     * 
     * @see FilteredItemsSelectionDialog.RefreshJob
     * 
     */
    private class RefreshCacheJob extends Job {

        private RefreshJob refreshJob = new RefreshJob();

        /**
         * Creates a new instance of the class.
         */
        public RefreshCacheJob() {
            super(WorkbenchMessages.FilteredItemsSelectionDialog_cacheRefreshJob);
            setSystem(true);
        }

        /**
         * Stops the job and all sub-jobs.
         */
        public void cancelAll() {
            cancel();
            refreshJob.cancel();
        }

        @Override
        protected IStatus run(IProgressMonitor monitor) {
            if (monitor.isCanceled()) {
                return new Status(IStatus.CANCEL, 
                        WorkbenchPlugin.PI_WORKBENCH,
                        IStatus.CANCEL, EMPTY_STRING, null);
            }

            if (FilteredItemsSelectionDialog.this != null) {
                GranualProgressMonitor wrappedMonitor = 
                        new GranualProgressMonitor(monitor);
                FilteredItemsSelectionDialog.this.reloadCache(
                        true, wrappedMonitor);
            }

            if (!monitor.isCanceled()) {
                refreshJob.schedule();
            }

            return new Status(IStatus.OK, 
                    PlatformUI.PLUGIN_ID, IStatus.OK,
                    EMPTY_STRING, null);

        }

        @Override
        protected void canceling() {
            super.canceling();
            contentProvider.stopReloadingCache();
        }

    }

    private class RemoveHistoryItemAction extends Action {

        public RemoveHistoryItemAction() {
            super(WorkbenchMessages.FilteredItemsSelectionDialog_removeItemsFromHistoryAction);
        }

        @Override
        public void run() {
            StructuredSelection selection = 
                    (StructuredSelection) list.getSelection();
            removeSelectedItems(selection.toList());
        }
    }

    private static boolean showColoredLabels() {
        return PlatformUI.getPreferenceStore()
                .getBoolean(IWorkbenchPreferenceConstants.USE_COLORED_LABELS);
    }

    private class ItemsListLabelProvider 
            extends StyledCellLabelProvider
            implements ILabelProviderListener {

        private ILabelProvider provider;

        private ILabelDecorator selectionDecorator;

        // Need to keep our own list of listeners
        private ListenerList listeners = new ListenerList();

        /**
         * Creates a new instance of the class.
         * 
         * @param provider
         *            the label provider for all items, not <code>null</code>
         * @param selectionDecorator
         *            the decorator for selected items, can be <code>null</code>
         */
        public ItemsListLabelProvider(ILabelProvider provider,
                ILabelDecorator selectionDecorator) {
            Assert.isNotNull(provider);
            this.provider = provider;
            this.selectionDecorator = selectionDecorator;

            setOwnerDrawEnabled(showColoredLabels() && 
                    provider instanceof IStyledLabelProvider);

            provider.addListener(this);

            if (selectionDecorator != null) {
                selectionDecorator.addListener(this);
            }
        }
        
        private Image getImage(Object element) {
            if (element instanceof ItemsListSeparator) {
                return SEP_IMAGE;
            }

            return provider.getImage(element);
        }

        private boolean isSelected(Object element) {
            if (element != null && currentSelection != null) {
                for (int i = 0; i < currentSelection.length; i++) {
                    if (element.equals(currentSelection[i]))
                        return true;
                }
            }
            return false;
        }

        private String getText(Object element) {
            if (element instanceof ItemsListSeparator) {
                ItemsListSeparator sep = 
                        (ItemsListSeparator) element;
                return getSeparatorLabel(sep.getName());
            }

            String str = provider.getText(element);
            if (selectionDecorator != null && 
                    isSelected(element)) {
                return selectionDecorator.decorateText(
                        str.toString(), element);
            }

            return str;
        }

        private StyledString getStyledText(Object element,
                IStyledLabelProvider provider) {
            StyledString string = 
                    provider.getStyledText(element);

            if (selectionDecorator != null && 
                    isSelected(element)) {
                String decorated = 
                        selectionDecorator.decorateText(
                                string.getString(), element);
                return styleDecoratedString(decorated, null, string);
                // no need to add colors when element is selected
            }
            return string;
        }

        @Override
        public void update(ViewerCell cell) {
            Object element = cell.getElement();
            Font openFont = CeylonPlugin.getOpenDialogFont();
            if (!(element instanceof ItemsListSeparator)
                    && provider instanceof IStyledLabelProvider) {
                IStyledLabelProvider styledLabelProvider = 
                        (IStyledLabelProvider) provider;
                StyledString styledString = 
                        getStyledText(element,
                                styledLabelProvider);

                cell.setText(styledString.getString());
                StyleRange[] styleRanges = 
                        styledString.getStyleRanges();
//                for (StyleRange styleRange : styleRanges) {
//                    styleRange.font = openFont;
//                }
                cell.setStyleRanges(styleRanges);
                cell.setImage(styledLabelProvider.getImage(element));
            }
            else {
                cell.setText(getText(element));
                cell.setStyleRanges(null); //work around bug in Eclipse!!
                cell.setImage(getImage(element));
            }
//            cell.setFont(getFont(element));
            cell.setFont(openFont);
            cell.setForeground(getForeground(element));
            cell.setBackground(getBackground(element));

            super.update(cell);
        }

        private String getSeparatorLabel(String separatorLabel) {
            Rectangle rect = list.getTable().getBounds();

            int borderWidth = 
                    list.getTable().computeTrim(0, 0, 0, 0).width;

            int imageWidth = SEP_IMAGE.getBounds().width;

            int width = rect.width - borderWidth - imageWidth;

            GC gc = new GC(list.getTable());
            gc.setFont(CeylonPlugin.getOpenDialogFont());

            int fSeparatorWidth = gc.getAdvanceWidth('-');
            int fMessageLength = gc.textExtent(separatorLabel).x;

            gc.dispose();

            StringBuffer dashes = new StringBuffer();
            int chars = (((width - fMessageLength) / fSeparatorWidth) / 2) - 2;
            for (int i = 0; i < chars; i++) {
                dashes.append('-');
            }

            StringBuffer result = new StringBuffer();
            result.append(dashes);
            result.append(" " + separatorLabel + " ");
            result.append(dashes);
            return result.toString().trim();
        }

        @Override
        public void addListener(ILabelProviderListener listener) {
            listeners.add(listener);
        }

        @Override
        public void dispose() {
            provider.removeListener(this);
            provider.dispose();

            if (selectionDecorator != null) {
                selectionDecorator.removeListener(this);
                selectionDecorator.dispose();
            }

            super.dispose();
        }

        @Override
        public boolean isLabelProperty(Object element, String property) {
            if (provider.isLabelProperty(element, property)) {
                return true;
            }
            if (selectionDecorator != null && 
                    selectionDecorator.isLabelProperty(
                            element, property)) {
                return true;
            }
            return false;
        }

        @Override
        public void removeListener(ILabelProviderListener listener) {
            listeners.remove(listener);
        }

        private Color getBackground(Object element) {
            if (element instanceof ItemsListSeparator) {
                return null;
            }
            if (provider instanceof IColorProvider) {
                return ((IColorProvider) provider).getBackground(element);
            }
            return null;
        }

        private Color getForeground(Object element) {
            if (element instanceof ItemsListSeparator) {
                return Display.getCurrent().getSystemColor(
                        SWT.COLOR_WIDGET_NORMAL_SHADOW);
            }
            if (provider instanceof IColorProvider) {
                return ((IColorProvider) provider).getForeground(element);
            }
            return null;
        }

        private Font getFont(Object element) {
            if (element instanceof ItemsListSeparator) {
                return null;
            }
            if (provider instanceof IFontProvider) {
                return ((IFontProvider) provider).getFont(element);
            }
            return null;
        }

        @Override
        public void labelProviderChanged(LabelProviderChangedEvent event) {
            Object[] l = listeners.getListeners();
            for (int i = 0; i < listeners.size(); i++) {
                ((ILabelProviderListener) l[i]).labelProviderChanged(event);
            }
        }
    }

    /**
     * Used in ItemsListContentProvider, separates history and non-history
     * items.
     */
    private class ItemsListSeparator {

        private String name;

        /**
         * Creates a new instance of the class.
         * 
         * @param name
         *            the name of the separator
         */
        public ItemsListSeparator(String name) {
            this.name = name;
        }

        /**
         * Returns the name of this separator.
         * 
         * @return the name of the separator
         */
        public String getName() {
            return name;
        }
    }

    /**
     * GranualProgressMonitor is used for monitoring progress of filtering
     * process. It is used by <code>RefreshProgressMessageJob</code> to
     * refresh progress message. State of this monitor illustrates state of
     * filtering or cache refreshing process.
     * 
     */
    private class GranualProgressMonitor extends ProgressMonitorWrapper {

        private String name;

        private String subName;

        private int totalWork;

        private double worked;

        private boolean done;

        /**
         * Creates instance of <code>GranualProgressMonitor</code>.
         * 
         * @param monitor
         *            progress to be wrapped
         */
        public GranualProgressMonitor(IProgressMonitor monitor) {
            super(monitor);
        }

        /**
         * Checks if filtering has been done
         * 
         * @return true if filtering work has been done false in other way
         */
        public boolean isDone() {
            return done;
        }

        @Override
        public void setTaskName(String name) {
            super.setTaskName(name);
            this.name = name;
            this.subName = null;
        }

        @Override
        public void subTask(String name) {
            super.subTask(name);
            this.subName = name;
        }

        @Override
        public void beginTask(String name, int totalWork) {
            super.beginTask(name, totalWork);
            if (this.name == null)
                this.name = name;
            this.totalWork = totalWork;
            refreshProgressMessageJob.scheduleProgressRefresh(this);
        }

        @Override
        public void worked(int work) {
            super.worked(work);
            internalWorked(work);
        }

        @Override
        public void done() {
            done = true;
            super.done();
        }

        @Override
        public void setCanceled(boolean b) {
            done = b;
            super.setCanceled(b);
        }

        @Override
        public void internalWorked(double work) {
            worked = worked + work;
        }

        /*private String getMessage() {
            if (done)
                return "";

            String message;

            if (name == null) {
                message = subName == null ? "" : subName;
            } else {
                message = subName == null ? name
                        : NLS
                                .bind(
                                        WorkbenchMessages.FilteredItemsSelectionDialog_subtaskProgressMessage,
                                        new Object[] { name, subName });
            }
            if (totalWork == 0)
                return message;

            return NLS
                    .bind(
                            WorkbenchMessages.FilteredItemsSelectionDialog_taskProgressMessage,
                            new Object[] {
                                    message,
                                    new Integer(
                                            (int) ((worked * 100) / totalWork)) });

        }*/

    }

    /**
     * Filters items history and schedule filter job.
     */
    private class FilterHistoryJob extends Job {

        /**
         * Filter used during the filtering process.
         */
        private ItemsFilter itemsFilter;

        /**
         * Creates new instance of receiver.
         */
        public FilterHistoryJob() {
            super(WorkbenchMessages.FilteredItemsSelectionDialog_jobLabel);
            setSystem(true);
        }

        @Override
        protected IStatus run(IProgressMonitor monitor) {

            this.itemsFilter = filter;

            contentProvider.reset();

            refreshWithLastSelection = false;

            contentProvider.addHistoryItems(itemsFilter);

            if (!(lastCompletedFilter != null && lastCompletedFilter
                    .isSubFilter(this.itemsFilter)))
                contentProvider.refresh();

            filterJob.schedule();

            return Status.OK_STATUS;
        }

    }

    /**
     * Filters items in indicated set and history. During filtering, it
     * refreshes the dialog (progress monitor and elements list).
     * 
     * Depending on the filter, <code>FilterJob</code> decides which kind of
     * search will be run inside <code>filterContent</code>. If the last
     * filtering is done (last completed filter), is not null, and the new
     * filter is a sub-filter ({@link FilteredItemsSelectionDialog.ItemsFilter#isSubFilter(FilteredItemsSelectionDialog.ItemsFilter)})
     * of the last, then <code>FilterJob</code> only filters in the cache. If
     * it is the first filtering or the new filter isn't a sub-filter of the
     * last one, a full search is run.
     */
    private class FilterJob extends Job {

        /**
         * Filter used during the filtering process.
         */
        protected ItemsFilter itemsFilter;

        /**
         * Creates new instance of FilterJob
         */
        public FilterJob() {
            super(WorkbenchMessages.FilteredItemsSelectionDialog_jobLabel);
            setSystem(true);
        }

        @Override
        protected final IStatus run(IProgressMonitor parent) {
            GranualProgressMonitor monitor = new GranualProgressMonitor(parent);
            return doRun(monitor);
        }

        /**
         * Executes job using the given filtering progress monitor. A hook for
         * subclasses.
         * 
         * @param monitor
         *            progress monitor
         * @return result of the execution
         */
        protected IStatus doRun(GranualProgressMonitor monitor) {
            try {
                internalRun(monitor);
            } catch (CoreException e) {
                cancel();
                return new Status(
                        IStatus.ERROR,
                        PlatformUI.PLUGIN_ID,
                        IStatus.ERROR,
                        WorkbenchMessages.FilteredItemsSelectionDialog_jobError,
                        e);
            }
            return Status.OK_STATUS;
        }

        /**
         * Main method for the job.
         * 
         * @param monitor
         * @throws CoreException
         */
        private void internalRun(GranualProgressMonitor monitor)
                throws CoreException {
            try {
                if (monitor.isCanceled())
                    return;

                this.itemsFilter = filter;

                if (filter.getPattern().length() != 0) {
                    filterContent(monitor);
                }

                if (monitor.isCanceled())
                    return;

                contentProvider.refresh();
            } finally {
                monitor.done();
            }
        }

        /**
         * Filters items.
         * 
         * @param monitor
         *            for monitoring progress
         * @throws CoreException
         */
        protected void filterContent(GranualProgressMonitor monitor)
                throws CoreException {

            if (lastCompletedFilter != null
                    && lastCompletedFilter.isSubFilter(this.itemsFilter)) {

                int length = lastCompletedResult.size() / 500;
                monitor.beginTask("Searching in cache", length);

                for (int pos = 0; pos < lastCompletedResult.size(); pos++) {
                    Object item = lastCompletedResult.get(pos);
                    if (monitor.isCanceled())
                        break;
                    contentProvider.add(item, itemsFilter);
                    if ((pos % 500) == 0) {
                        monitor.worked(1);
                    }
                }

            } else {

                lastCompletedFilter = null;
                lastCompletedResult = null;

                SubProgressMonitor subMonitor = null;
                if (monitor != null) {
                    monitor.beginTask("Searching", 100);
                    subMonitor = new SubProgressMonitor(monitor, 95);
                }

                fillContentProvider(contentProvider, itemsFilter, subMonitor);

                if (monitor != null && !monitor.isCanceled()) {
                    monitor.worked(2);
                    contentProvider.rememberResult(itemsFilter);
                    monitor.worked(3);
                }
            }

        }

    }

    /**
     * History stores a list of key, object pairs. The list is bounded at a
     * certain size. If the list exceeds this size the oldest element is removed
     * from the list. An element can be added/renewed with a call to
     * <code>accessed(Object)</code>.
     * <p>
     * The history can be stored to/loaded from an XML file.
     */
    protected static abstract class SelectionHistory {

        private static final String DEFAULT_ROOT_NODE_NAME = "historyRootNode";

        private static final String DEFAULT_INFO_NODE_NAME = "infoNode";

        private static final int MAX_HISTORY_SIZE = 60;

        private final Set<Object> historyList;

        private final String rootNodeName;

        private final String infoNodeName;

        private SelectionHistory(String rootNodeName, String infoNodeName) {

            historyList = Collections.synchronizedSet(new LinkedHashSet<Object>() {

                private static final long serialVersionUID = 0L;

                @Override
                public boolean add(Object arg0) {
                    if (this.size() >= MAX_HISTORY_SIZE) {
                        Iterator<Object> iterator = this.iterator();
                        iterator.next();
                        iterator.remove();
                    }
                    return super.add(arg0);
                }

            });

            this.rootNodeName = rootNodeName;
            this.infoNodeName = infoNodeName;
        }

        /**
         * Creates new instance of <code>SelectionHistory</code>.
         */
        public SelectionHistory() {
            this(DEFAULT_ROOT_NODE_NAME, DEFAULT_INFO_NODE_NAME);
        }

        /**
         * Adds object to history.
         * 
         * @param object
         *            the item to be added to the history
         */
        public synchronized void accessed(Object object) {
            historyList.remove(object);
            historyList.add(object);
        }

        /**
         * Returns <code>true</code> if history contains object.
         * 
         * @param object
         *            the item for which check will be executed
         * @return <code>true</code> if history contains object
         *         <code>false</code> in other way
         */
        public synchronized boolean contains(Object object) {
            return historyList.contains(object);
        }

        /**
         * Returns <code>true</code> if history is empty.
         * 
         * @return <code>true</code> if history is empty
         */
        public synchronized boolean isEmpty() {
            return historyList.isEmpty();
        }

        /**
         * Remove element from history.
         * 
         * @param element
         *            to remove form the history
         * @return <code>true</code> if this list contained the specified
         *         element
         */
        public synchronized boolean remove(Object element) {
            return historyList.remove(element);
        }

        /**
         * Load history elements from memento.
         * 
         * @param memento
         *            memento from which the history will be retrieved
         */
        public void load(IMemento memento) {

            XMLMemento historyMemento = (XMLMemento) memento
                    .getChild(rootNodeName);

            if (historyMemento == null) {
                return;
            }

            IMemento[] mementoElements = historyMemento
                    .getChildren(infoNodeName);
            for (int i = 0; i < mementoElements.length; ++i) {
                IMemento mementoElement = mementoElements[i];
                Object object = restoreItemFromMemento(mementoElement);
                if (object != null) {
                    historyList.add(object);
                }
            }
        }

        /**
         * Save history elements to memento.
         * 
         * @param memento
         *            memento to which the history will be added
         */
        public void save(IMemento memento) {

            IMemento historyMemento = memento.createChild(rootNodeName);

            Object[] items = getHistoryItems();
            for (int i = 0; i < items.length; i++) {
                Object item = items[i];
                IMemento elementMemento = historyMemento
                        .createChild(infoNodeName);
                storeItemToMemento(item, elementMemento);
            }

        }

        /**
         * Gets array of history items.
         * 
         * @return array of history elements
         */
        public synchronized Object[] getHistoryItems() {
            return historyList.toArray();
        }

        /**
         * Creates an object using given memento.
         * 
         * @param memento
         *            memento used for creating new object
         * 
         * @return the restored object
         */
        protected abstract Object restoreItemFromMemento(IMemento memento);

        /**
         * Store object in <code>IMemento</code>.
         * 
         * @param item
         *            the item to store
         * @param memento
         *            the memento to store to
         */
        protected abstract void storeItemToMemento(Object item, IMemento memento);

    }

    /**
     * Filters elements using SearchPattern by comparing the names of items with
     * the filter pattern.
     */
    protected abstract class ItemsFilter {

        protected SearchPattern patternMatcher;

        /**
         * Creates new instance of ItemsFilter.
         */
        public ItemsFilter() {
            this(new SearchPattern());
        }

        /**
         * Creates new instance of ItemsFilter.
         * 
         * @param searchPattern
         *            the pattern to be used when filtering
         */
        public ItemsFilter(SearchPattern searchPattern) {
            patternMatcher = searchPattern;
            String stringPattern = "";
            if (pattern != null && !pattern.getText().equals("*")) {
                stringPattern = pattern.getText();
            }
            patternMatcher.setPattern(stringPattern);
        }

        /**
         * Check if the given filter is a sub-filter of this filter. The default
         * implementation checks if the <code>SearchPattern</code> from the
         * given filter is a sub-pattern of the one from this filter.
         * <p>
         * <i>WARNING: This method is <b>not</b> defined in reading order, i.e.
         * <code>a.isSubFilter(b)</code> is <code>true</code> iff
         * <code>b</code> is a sub-filter of <code>a</code>, and not
         * vice-versa. </i>
         * </p>
         * 
         * @param filter
         *            the filter to be checked, or <code>null</code>
         * @return <code>true</code> if the given filter is sub-filter of this
         *         filter, <code>false</code> if the given filter isn't a
         *         sub-filter or is <code>null</code>
         * 
         * @see org.eclipse.ui.dialogs.SearchPattern#isSubPattern(org.eclipse.ui.dialogs.SearchPattern)
         */
        public boolean isSubFilter(ItemsFilter filter) {
            if (filter != null) {
                return this.patternMatcher.isSubPattern(filter.patternMatcher);
            }
            return false;
        }

        /**
         * Checks whether the provided filter is equal to the current filter.
         * The default implementation checks if <code>SearchPattern</code>
         * from current filter is equal to the one from provided filter.
         * 
         * @param filter
         *            filter to be checked, or <code>null</code>
         * @return <code>true</code> if the given filter is equal to current
         *         filter, <code>false</code> if given filter isn't equal to
         *         current one or if it is <code>null</code>
         * 
         * @see org.eclipse.ui.dialogs.SearchPattern#equalsPattern(org.eclipse.ui.dialogs.SearchPattern)
         */
        public boolean equalsFilter(ItemsFilter filter) {
            if (filter != null
                    && filter.patternMatcher.equalsPattern(this.patternMatcher)) {
                return true;
            }
            return false;
        }

        /**
         * Checks whether the pattern's match rule is camel case.
         * 
         * @return <code>true</code> if pattern's match rule is camel case,
         *         <code>false</code> otherwise
         */
        public boolean isCamelCasePattern() {
            return patternMatcher.getMatchRule() == SearchPattern.RULE_CAMELCASE_MATCH;
        }

        /**
         * Returns the pattern string.
         * 
         * @return pattern for this filter
         * 
         * @see SearchPattern#getPattern()
         */
        public String getPattern() {
            return patternMatcher.getPattern();
        }

        /**
         * Returns the rule to apply for matching keys.
         * 
         * @return an implementation-specific match rule
         * 
         * @see SearchPattern#getMatchRule() for match rules returned by the
         *      default implementation
         */
        public int getMatchRule() {
            return patternMatcher.getMatchRule();
        }

        /**
         * Matches text with filter.
         * 
         * @param text
         *            the text to match with the filter
         * @return <code>true</code> if text matches with filter pattern,
         *         <code>false</code> otherwise
         */
        protected boolean matches(String text) {
            return patternMatcher.matches(text);
        }

        /**
         * Matches an item against filter conditions.
         * 
         * @param item
         * @return <code>true<code> if item matches against filter conditions, <code>false</code>
         *         otherwise
         */
        public abstract boolean matchItem(Object item);

        /**
         * Checks consistency of an item. Item is inconsistent if was changed or
         * removed.
         * 
         * @param item
         * @return <code>true</code> if item is consistent, <code>false</code>
         *         if item is inconsistent
         */
        public abstract boolean isConsistentItem(Object item);

    }

    /**
     * An interface to content providers for
     * <code>FilterItemsSelectionDialog</code>.
     */
    protected abstract class AbstractContentProvider {
        /**
         * Adds the item to the content provider iff the filter matches the
         * item. Otherwise does nothing.
         * 
         * @param item
         *            the item to add
         * @param itemsFilter
         *            the filter
         * 
         * @see FilteredItemsSelectionDialog.ItemsFilter#matchItem(Object)
         */
        public abstract void add(Object item, ItemsFilter itemsFilter);
    }

    /**
     * Collects filtered elements. Contains one synchronized, sorted set for
     * collecting filtered elements. All collected elements are sorted using
     * comparator. Comparator is returned by getElementComparator() method.
     * Implementation of <code>ItemsFilter</code> is used to filter elements.
     * The key function of filter used in to filtering is
     * <code>matchElement(Object item)</code>.
     * <p>
     * The <code>ContentProvider</code> class also provides item filtering
     * methods. The filtering has been moved from the standard TableView
     * <code>getFilteredItems()</code> method to content provider, because
     * <code>ILazyContentProvider</code> and virtual tables are used. This
     * class is responsible for adding a separator below history items and
     * marking each items as duplicate if its name repeats more than once on the
     * filtered list.
     */
    private class ContentProvider extends AbstractContentProvider implements
            IStructuredContentProvider, ILazyContentProvider {

        private SelectionHistory selectionHistory;

        /**
         * Raw result of the searching (unsorted, unfiltered).
         * <p>
         * Standard object flow:
         * <code>items -> lastSortedItems -> lastFilteredItems</code>
         */
        private Set<Object> items;

        /**
         * Items that are duplicates.
         */
        private Set<Object> duplicates;

        /**
         * List of <code>ViewerFilter</code>s to be used during filtering
         */
        private List<ViewerFilter> filters;

        /**
         * Result of the last filtering.
         * <p>
         * Standard object flow:
         * <code>items -> lastSortedItems -> lastFilteredItems</code>
         */
        private List<Object> lastFilteredItems;

        /**
         * Result of the last sorting.
         * <p>
         * Standard object flow:
         * <code>items -> lastSortedItems -> lastFilteredItems</code>
         */
        private List<Object> lastSortedItems;

        /**
         * Used for <code>getFilteredItems()</code> method canceling (when the
         * job that invoked the method was canceled).
         * <p>
         * Function canceling could be based (only) on monitor canceling
         * unfortunately sometimes the method <code>getFilteredElements()</code>
         * could be run with a null monitor, the <code>reset</code> flag have
         * to be left intact.
         */
        private boolean reset;

        /**
         * Creates new instance of <code>ContentProvider</code>.
         */
        public ContentProvider() {
            this.items = Collections.synchronizedSet(new HashSet<Object>(2048));
            this.duplicates = Collections.synchronizedSet(new HashSet<Object>(256));
            this.lastFilteredItems = new ArrayList<Object>();
            this.lastSortedItems = Collections.synchronizedList(new ArrayList<Object>(2048));
        }

        /**
         * Sets selection history.
         * 
         * @param selectionHistory
         *            The selectionHistory to set.
         */
        public void setSelectionHistory(SelectionHistory selectionHistory) {
            this.selectionHistory = selectionHistory;
        }

        /**
         * @return Returns the selectionHistory.
         */
        public SelectionHistory getSelectionHistory() {
            return selectionHistory;
        }

        /**
         * Removes all content items and resets progress message.
         */
        public void reset() {
            reset = true;
            this.items.clear();
            this.duplicates.clear();
            this.lastSortedItems.clear();
        }

        /**
         * Stops reloading cache - <code>getFilteredItems()</code> method.
         */
        public void stopReloadingCache() {
            reset = true;
        }

        /**
         * Adds filtered item.
         * 
         * @param item
         * @param itemsFilter
         */
        @Override
        public void add(Object item, ItemsFilter itemsFilter) {
            if (itemsFilter == filter) {
                if (itemsFilter != null) {
                    if (itemsFilter.matchItem(item)) {
                        this.items.add(item);
                    }
                } else {
                    this.items.add(item);
                }
            }
        }

        /**
         * Add all history items to <code>contentProvider</code>.
         * 
         * @param itemsFilter
         */
        public void addHistoryItems(ItemsFilter itemsFilter) {
            if (this.selectionHistory != null) {
                Object[] items = 
                        this.selectionHistory.getHistoryItems();
                for (int i = 0; i < items.length; i++) {
                    Object item = items[i];
                    if (itemsFilter == filter) {
                        if (itemsFilter != null) {
                            if (itemsFilter.matchItem(item)) {
                                if (itemsFilter.isConsistentItem(item)) {
                                    this.items.add(item);
                                } else {
                                    this.selectionHistory.remove(item);
                                }
                            }
                        }
                    }
                }
            }
        }

        /**
         * Refresh dialog.
         */
        public void refresh() {
            scheduleRefresh();
        }

        /**
         * Removes items from history and refreshes the view.
         * 
         * @param item
         *            to remove
         * 
         * @return removed item
         */
        public Object removeHistoryElement(Object item) {
            if (this.selectionHistory != null)
                this.selectionHistory.remove(item);
            if (filter == null || 
                    filter.getPattern().length() == 0) {
                items.remove(item);
                duplicates.remove(item);
                this.lastSortedItems.remove(item);
            }

            synchronized (lastSortedItems) {
                Collections.sort(lastSortedItems, 
                        getHistoryComparator());
            }
            return item;
        }

        /**
         * Adds item to history and refresh view.
         * 
         * @param item
         *            to add
         */
        public void addHistoryElement(Object item) {
            if (this.selectionHistory != null)
                this.selectionHistory.accessed(item);
            if (filter == null || !filter.matchItem(item)) {
                this.items.remove(item);
                this.duplicates.remove(item);
                this.lastSortedItems.remove(item);
            }
            synchronized (lastSortedItems) {
                Collections.sort(lastSortedItems, 
                        getHistoryComparator());
            }
            this.refresh();
        }

        /**
         * @param item
         * @return <code>true</code> if given item is part of the history
         */
        public boolean isHistoryElement(Object item) {
            if (this.selectionHistory != null) {
                return this.selectionHistory.contains(item);
            }
            return false;
        }

        /**
         * Sets/unsets given item as duplicate.
         * 
         * @param item
         *            item to change
         * 
         * @param isDuplicate
         *            duplicate flag
         */
        public void setDuplicateElement(Object item, boolean isDuplicate) {
            if (this.items.contains(item)) {
                if (isDuplicate)
                    this.duplicates.add(item);
                else
                    this.duplicates.remove(item);
            }
        }

        /**
         * Indicates whether given item is a duplicate.
         * 
         * @param item
         *            item to check
         * @return <code>true</code> if item is duplicate
         */
        public boolean isDuplicateElement(Object item) {
            return duplicates.contains(item);
        }

        /**
         * Load history from memento.
         * 
         * @param memento
         *            memento from which the history will be retrieved
         */
        public void loadHistory(IMemento memento) {
            if (this.selectionHistory != null)
                this.selectionHistory.load(memento);
        }

        /**
         * Save history to memento.
         * 
         * @param memento
         *            memento to which the history will be added
         */
        public void saveHistory(IMemento memento) {
            if (this.selectionHistory != null)
                this.selectionHistory.save(memento);
        }

        /**
         * Gets sorted items.
         * 
         * @return sorted items
         */
        private Object[] getSortedItems() {
            if (lastSortedItems.size() != items.size()) {
                synchronized (lastSortedItems) {
                    lastSortedItems.clear();
                    lastSortedItems.addAll(items);
                    Collections.sort(lastSortedItems, 
                            getHistoryComparator());
                }
            }
            return lastSortedItems.toArray();
        }

        /**
         * Remember result of filtering.
         * 
         * @param itemsFilter
         */
        public void rememberResult(ItemsFilter itemsFilter) {
            List<Object> itemsList = 
                    Collections.synchronizedList(
                            Arrays.asList(getSortedItems()));
            // synchronization
            if (itemsFilter == filter) {
                lastCompletedFilter = itemsFilter;
                lastCompletedResult = itemsList;
            }

        }

        @Override
        public Object[] getElements(Object inputElement) {
            return lastFilteredItems.toArray();
        }

        public int getNumberOfElements() {
            return lastFilteredItems.size();
        }

        @Override
        public void dispose() {}

        @Override
        public void inputChanged(Viewer viewer, 
                Object oldInput, Object newInput) {}

        @Override
        public void updateElement(int index) {

            FilteredItemsSelectionDialog.this.list.replace(
                    lastFilteredItems.size() > index ? 
                            lastFilteredItems.get(index) : null,
                    index);

        }

        /**
         * Main method responsible for getting the filtered items and checking
         * for duplicates. It is based on the
         * {@link FilteredItemsSelectionDialog.ContentProvider#getFilteredItems(Object, IProgressMonitor)}.
         * 
         * @param checkDuplicates
         *            <code>true</code> if data concerning elements
         *            duplication should be computed - it takes much more time
         *            than standard filtering
         * 
         * @param monitor
         *            progress monitor
         */
        public void reloadCache(boolean checkDuplicates,
                IProgressMonitor monitor) {

            reset = false;

            if (monitor != null) {
                // the work is divided into two actions of the same length
                int totalWork = checkDuplicates ? 200 : 100;

                monitor
                        .beginTask(
                                WorkbenchMessages.FilteredItemsSelectionDialog_cacheRefreshJob,
                                totalWork);
            }

            // the TableViewer's root (the input) is treated as parent

            lastFilteredItems = Arrays.asList(getFilteredItems(list.getInput(),
                    monitor != null ? new SubProgressMonitor(monitor, 100)
                            : null));

            if (reset || (monitor != null && monitor.isCanceled())) {
                if (monitor != null)
                    monitor.done();
                return;
            }

            if (checkDuplicates) {
                checkDuplicates(monitor);
            }
            if (monitor != null)
                monitor.done();
        }

        private void checkDuplicates(IProgressMonitor monitor) {
            synchronized (lastFilteredItems) {
                IProgressMonitor subMonitor = null;
                int reportEvery = lastFilteredItems.size() / 20;
                if (monitor != null) {
                    subMonitor = new SubProgressMonitor(monitor, 100);
                    subMonitor
                            .beginTask(
                                    WorkbenchMessages.FilteredItemsSelectionDialog_cacheRefreshJob_checkDuplicates,
                                    5);
                }
                HashMap<Object,Object> helperMap = 
                        new HashMap<Object,Object>();
                for (int i = 0; i < lastFilteredItems.size(); i++) {
                    if (reset || 
                            (subMonitor != null && 
                             subMonitor.isCanceled()))
                        return;
                    Object item = lastFilteredItems.get(i);

                    if (!(item instanceof ItemsListSeparator)) {
                        Object previousItem = helperMap.put(
                                getElementName(item), item);
                        if (previousItem != null) {
                            setDuplicateElement(previousItem, true);
                            setDuplicateElement(item, true);
                        } else {
                            setDuplicateElement(item, false);
                        }
                    }

                    if (subMonitor != null && reportEvery != 0
                            && (i + 1) % reportEvery == 0)
                        subMonitor.worked(1);
                }
                helperMap.clear();
            }
        }

        /**
         * Returns an array of items filtered using the provided
         * <code>ViewerFilter</code>s with a separator added.
         * 
         * @param parent
         *            the parent
         * @param monitor
         *            progress monitor, can be <code>null</code>
         * @return an array of filtered items
         */
        protected Object[] getFilteredItems(Object parent,
                IProgressMonitor monitor) {
            int ticks = 100;
            if (monitor == null) {
                monitor = new NullProgressMonitor();
            }

            monitor
                    .beginTask(
                            WorkbenchMessages.FilteredItemsSelectionDialog_cacheRefreshJob_getFilteredElements,
                            ticks);
            if (filters != null) {
                ticks /= (filters.size() + 2);
            } else {
                ticks /= 2;
            }

            // get already sorted array
            Object[] filteredElements = getSortedItems();

            monitor.worked(ticks);

            // filter the elements using provided ViewerFilters
            if (filters != null && filteredElements != null) {
                for (Iterator<ViewerFilter> iter = 
                        filters.iterator(); 
                        iter.hasNext();) {
                    ViewerFilter f = iter.next();
                    filteredElements = 
                            f.filter(list, parent, 
                                    filteredElements);
                    monitor.worked(ticks);
                }
            }

            if (filteredElements == null || 
                    monitor.isCanceled()) {
                monitor.done();
                return new Object[0];
            }

            ArrayList<Object> preparedElements = 
                    new ArrayList<Object>();
            boolean hasHistory = false;

            if (filteredElements.length > 0) {
                if (isHistoryElement(filteredElements[0])) {
                    hasHistory = true;
                }
            }

            int reportEvery = filteredElements.length / ticks;

            // add separator
            for (int i = 0; i < filteredElements.length; i++) {
                Object item = filteredElements[i];

                if (hasHistory && !isHistoryElement(item)) {
                    preparedElements.add(itemsListSeparator);
                    hasHistory = false;
                }

                preparedElements.add(item);

                if (reportEvery != 0 && ((i + 1) % reportEvery == 0)) {
                    monitor.worked(1);
                }
            }

            monitor.done();

            return preparedElements.toArray();
        }

        /**
         * Adds a filter to this content provider. For an example usage of such
         * filters look at the project <code>org.eclipse.ui.ide</code>, class
         * <code>org.eclipse.ui.dialogs.FilteredResourcesSelectionDialog.CustomWorkingSetFilter</code>.
         * 
         * 
         * @param filter
         *            the filter to be added
         */
        public void addFilter(ViewerFilter filter) {
            if (filters == null) {
                filters = new ArrayList<ViewerFilter>();
            }
            filters.add(filter);
            // currently filters are only added when dialog is restored
            // if it is changed, refreshing the whole TableViewer should be
            // added
        }

    }

    /**
     * A content provider that does nothing.
     */
    private class NullContentProvider implements IContentProvider {

        @Override
        public void dispose() {
        }

        @Override
        public void inputChanged(Viewer viewer, 
                Object oldInput, Object newInput) {
        }

    }

    /**
     * DetailsContentViewer objects are wrappers for labels.
     * DetailsContentViewer provides means to change label's image and text when
     * the attached LabelProvider is updated.
     */
    private class DetailsContentViewer extends ContentViewer {

        private CLabel label;

        /**
         * Unfortunately, it was impossible to delegate displaying border to
         * label. The <code>ViewForm</code> is used because
         * <code>CLabel</code> displays shadow when border is present.
         */
//        private ViewForm viewForm;

        /**
         * Constructs a new instance of this class given its parent and a style
         * value describing its behavior and appearance.
         * 
         * @param parent
         *            the parent component
         * @param style
         *            SWT style bits
         */
        public DetailsContentViewer(Composite parent, int style) {
//            viewForm = new ViewForm(parent, style);
//            GridData gd = new GridData(GridData.FILL_HORIZONTAL);
//            gd.horizontalSpan = 1;
//            viewForm.setLayoutData(gd);
            label = new CLabel(parent, SWT.FLAT);
            label.setFont(parent.getFont());
            label.setLayoutData(GridDataFactory.fillDefaults().create());
//            viewForm.setContent(label);
            hookControl(label);
        }

        @Override
        protected void inputChanged(Object input, Object oldInput) {
            if (oldInput == null) {
                if (input == null) {
                    return;
                }
                refresh();
                return;
            }

            refresh();

        }

        @Override
        protected void handleLabelProviderChanged(
                LabelProviderChangedEvent event) {
            if (event != null) {
                refresh(event.getElements());
            }
        }

        @Override
        public Control getControl() {
            return label;
        }

        @Override
        public ISelection getSelection() {
            // not supported
            return null;
        }

        @Override
        public void refresh() {
            Object input = this.getInput();
            if (input != null) {
                ILabelProvider labelProvider = 
                        (ILabelProvider) getLabelProvider();
                doRefresh(labelProvider.getText(input), 
                        labelProvider.getImage(input));
            }
            else {
                doRefresh(null, null);
            }
        }

        /**
         * Sets the given text and image to the label.
         * 
         * @param text
         *            the new text or null
         * @param image
         *            the new image
         */
        private void doRefresh(String text, Image image) {
            if ( text != null ) {
                text = LegacyActionTools.escapeMnemonics(text);
            }
            label.setText(text);
            label.setImage(image);
        }

        @Override
        public void setSelection(ISelection selection, boolean reveal) {
            // not supported
        }

        /**
         * Refreshes the label if currently chosen element is on the list.
         * 
         * @param objs
         *            list of changed object
         */
        private void refresh(Object[] objs) {
            if (objs == null || getInput() == null) {
                return;
            }
            Object input = getInput();
            for (int i = 0; i < objs.length; i++) {
                if (objs[i].equals(input)) {
                    refresh();
                    break;
                }
            }
        }
    }

    /**
     * Compares items according to the history.
     */
    private class HistoryComparator implements Comparator<Object> {

        @Override
        public int compare(Object o1, Object o2) {
            boolean h1 = isHistoryElement(o1);
            boolean h2 = isHistoryElement(o2);
            if (h1 == h2)
                return getItemsComparator().compare(o1, o2);

            if (h1)
                return -2;
            if (h2)
                return +2;

            return 0;
        }

    }
    

    /**
     * Get the control where the search pattern is entered. Any filtering should
     * be done using an {@link ItemsFilter}. This control should only be
     * accessed for listeners that wish to handle events that do not affect
     * filtering such as custom traversal.
     * 
     * @return Control or <code>null</code> if the pattern control has not
     *         been created.
     */
    public Text getPatternControl() {
        return pattern;
    }

    void handleLink(String location, DocBrowser browser) {}

}
